异常的辨识
典型情况下,在一个程序里可能存在多种不同的运行时错误,我们可以将这些错误映射到一些具有不同名字的异常。我喜欢定义一些除了服务于异常处理之外没有其他用途的类,这使人不容易将它们的用途弄错。特别地,我绝不去用内部类型(例如,int)作为异常。因为,在一个大程序里,我没有有效的方法去确定int异常的无关使用,这样,我也就无法保证那些另有所图的使用不会与我的使用相互干扰。
我们的计算器(6.1节)必须处理两类运行时的错误:语法错误和企图除以0的错误。检查到企图除以0错误的代码不需要给处理器传递任何信息,因此,除以0的问题可以用一个简单的空类型表示:
struct Zero_divide { };
而在另一方面,处理器非常希望能得到一个有关出现了什么语法错误的指示。在这里我们就传递一个字符串
struct Syntax_error {
const char* p;
Syntax_error(const char* q) { p = q; }
};
为了记述上的方便,我给这个struct加进了一个构造函数(2.5.2节、10.2.3节)。
分析器的用户可以通过在try块之后附加两个处理器的方式,完成对这两种异常的辨识,在需要时控制就会进入适当的处理器。如果我们从一个处理器的“末端掉出去”,执行就将从整个的处理器列表之后继续下去:
try {
// ...
expr(false);
// 当且仅当expr()没有导致任何异常,我们将到达这里
// ...
}
catch (Syntax_error) {
// 处理语法错误
}
catch (Zero_divide) {
// 处理用零除的错误
}
// 如果expr()没有发生任何异常,或者是出现Syntax_error
// 或Zero_divide异常并被捕捉到(而且其处理器不return,
// 不抛出异常,也不以其他方式改变控制流),我们就能到达这里
处理器的表看起来就像一个开关语句,但是这里不需要break。处理器列表在语法与case列表不同,部分的原因也就在于此,另一个原因是指明每个处理器都是一个作用域(4.9.4节)。
一个函数不必捕捉所有可能的异常。例如,前面的try块就没有打算去捕捉可能由分析器的输入操作产生的异常。这些异常将简单地“穿过”这里,继续去查找某个带有合适处理器的调用者。
从语言的观点看,被考虑那个异常正好在其处理器的入口进行处理,所以,在执行处理器期间所抛出的异常就必须由这个try块的调用者去处理。举个例子,下面的代码不会导致无穷循环:
class Input_overflow { /* ... */ };
void f()
{
try {
// ...
}
catch (Input_overflow) {
// ...
throw Input_overflow();
}
}
异常处理器也可以嵌套。例如,
class XXII { /* ... */ };
void f()
{
// ...
try {
// ...
}
catch (XXII) {
try {
// 某些复杂的东西
}
catch (XXII) {
// 复杂处理器代码失败
}
}
// ...
}
当然,在人们写出的代码中很少会看到这种嵌套,这更多的是表明了某种糟糕风格。
🔚